using System;
using System.Collections.Generic;
using System.Reflection;
using TestCases.CompareAssert.Core.Comparers.Interfaces;
using TestCases.CompareAssert.Core.Interfaces;
using TestCases.CompareAssert.Core.Selectors;
using TestCases.Utils;

namespace TestCases.CompareAssert.Core.Comparers.Members
{
    public abstract class MembersComparer<TMember> : IObjectComparer where TMember : MemberInfo
    {
        
        private readonly ITypeSelector _selector;
        
        protected MembersComparer() : this(SimpleTypeSelector.Instance)
        {            
        }
        
        protected MembersComparer(ITypeSelector selector)
        {
            AccessFlags = AccessFlags.Default;
            _selector = selector;
        }
                
        protected MembersComparer(IObjectComparer comparer) : this(SimpleTypeSelector.Instance, comparer)
        {            
        }

        protected MembersComparer(ITypeSelector typeSelector, IObjectComparer comparer) : this(typeSelector)
        {
            MemberComparer = comparer;
        }

        public IObjectComparer MemberComparer { get; set; }
        public AccessFlags AccessFlags { get; set; }
        public Predicate<string> IgnoreMember { get; set; }

        public abstract string EntityName { get; }
        public abstract IEnumerable<string> GetMemberNames(Type type, BindingFlags flags);
        public abstract TMember GetMember(Type type, string name, BindingFlags flags);
        public abstract object GetMemberValue(TMember member, object instance);
        public abstract string Name { get; }

        private IEnumerable<string> GetMemberNames(Type leftType, Type rightType)
        {
            var members = new HashSet<string>();
            var types = _selector.SelectFrom(leftType, rightType);
            foreach (var type in types)
            {
                members.AddAll(GetMemberNames(type, (BindingFlags) AccessFlags));
            }
            return members;            
        }


        public IResultBuilder Compare(ICompareContext context)
        {
            Type t1 = context.LType, t2 = context.RType;
            var members = GetMemberNames(t1, t2);            
            var results = new List<IResultProvider>();
            var isNotApplicable = true;

            foreach (var memberName in members)
            {

                if (IgnoreMember != null && IgnoreMember(memberName))
                {
                    continue;
                }
                
                TMember leftMember = GetMember(t1, memberName, (BindingFlags)AccessFlags), 
                        rightMember = GetMember(t2, memberName, (BindingFlags)AccessFlags);
                        
                if (leftMember == null)
                {
                    return this.IsNotEquals(context, results).
                        Message("<{LPath}> with type <{LType}> doesn't have <{MemberName}> " + EntityName + ".").
                        With(new {MemberName = memberName});
                }

                if (rightMember == null)
                {
                    return this.IsNotEquals(context, results).
                        Message("<{RPath}> with type <{RType}> doesn't have <{MemberName}> " + EntityName + ".").
                        With(new {MemberName = memberName});
                }
                
                object leftValue = GetMemberValue(leftMember, context.LValue), 
                       rightValue = GetMemberValue(rightMember, context.RValue);

                var membersContext = context.ForEntityObjects(leftValue, rightValue, memberName);
                var status = MemberComparer.CompareAndAddResult(membersContext, results).GetStatus();
                switch (status)
                {
                    case CompareStatus.NotEquals:
                        return this.IsNotEquals(context, results).
                            Message("<{LMemberPath}> is not equal to <{RMemberPath}> [{LMemberValue} != {RMemberValue}].").
                            With(new
                            {
                                LMemberPath = membersContext.LPath, 
                                RMemberPath = membersContext.RPath, 
                                LMemberValue = leftValue, 
                                RMemberValue = rightValue
                            });

                    case CompareStatus.Equals:
                        isNotApplicable = false;
                        break;
                }
            }
            return (isNotApplicable)
                ? this.IsNotApplicable(context, results).Message("<{LPath}> and <{RPath}> doesn't have any " + EntityName + ".")
                : this.IsEquals(context, results).Message("All <{LPath}> and <{RPath}> " + EntityName.ToPlural() + " are equals.");
        }

        public static string[] ExtractMemberNames(MemberInfo[] members)
        {
            return Array.ConvertAll(members, x => x.Name);
        }
    }
}